﻿/*
 * Stardust Software R&D Group Confidential
 * Project: Vesalius Image Annotation - Name as Salomay
 *
 * (C) Copyright Stardust Software R&D Group. 2013
 *
 * The source code for this program is not published or otherwise divested of 
 * its trade secrets
 */

/**
 * The implementation of interface Salomay.Engine and based on open source framework Fabric.
 * this class is an adapter to convert fabric.Canvas into Salomay interface
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.FabricEngineAdapter = Class.create();
Salomay.FabricEngineAdapter.prototype = Class.extend({
	/**
	 * Constructor
	 **/
	_initialize: function(options){
		this.engine = new fabric.Canvas(options.domNodeId, options);
		this.engine.preToObject = function (properties) {
			if (!properties) properties = new Array();
			// Add more 'toJSON' properties here...
			properties.push("id");
			properties.push("attachTo");
			properties.push("isConnectionLine");
			properties.push("attachedBy");
			properties.push("lastShapeIndex");
			return properties;
		}
	},
	
	/**
	 * Set property for engine instance
	 * @param name name of property
	 * @param value value of property
	 **/
	_setProperty: function(name, value){
		try {
			eval ("this.engine." + name + "= '" + value + "'");
		} catch (e) {
		}
	},
	
	/**
	 * Get property of engine instance
	 * @param name name of property
	 * @return value of property
	 **/
	_getProperty: function(name){
		var val = null;
		try {
			eval ("val = this.engine." + name);
		} catch (e) {
			
		}
		return val;
	},
	
	/** 
	 * Add fabric shape object or salomay shape object into fabric canvas
	 * @param Salomay shape object or fabric shape object
	 **/
	_addShape: function(options) {
		var shape = options.shape;
		if (shape.isFabricShapeAdapter) {
			shape.renderToCanvas(this);
		} else {
			this.engine.add(shape);
		}
	},
	
	/** 
	 * Remove fabric shape object or salomay shape object from fabric canvas and return the deleted object
	 * @param Salomay shape object or fabric shape object
	 **/
	_removeShape: function(options) {
		var shape = options.shape;
		if (shape.isFabricShapeAdapter) {
			return shape.removeFromCanvas(this);
		} else {
			return this.engine.remove(shape);
		}
	},
	
	/**
	 * Render all shapes added into canvas and refresh the graphic cache
	 * @param - options JSON
	 **/
	_renderAll: function(options){
		this.engine.renderAll();
	},
	
	/**
	 * Switch to free drawing mode to paint like pencil, spray
	 * @param - options JSON
	 **/
	_enableDrawingMode: function(options){
		this.engine.isDrawingMode = true;
	},
	
	/**
	 * Disable free drawing mode
	 * @param - options JSON
	 **/
	_disableDrawingMode: function(options){
		this.engine.isDrawingMode = false;
	},
	
	/**
	 * Is drawing mode
	 * @return see if the engine turns on drawing mode
	 **/
	
	_isDrawingMode: function(){
		return this.engine.isDrawingMode;
	},
	
	/**
	 * Set background image for engine
	 * @param JSON options contain image information
	 **/
	_setBackgroundImage: function(options) {
		var imageUrl = options.url;
		var callback = options.callback ? options.callback : (function(){this.engine.renderAll();}).bind(this);
		this.engine.setBackgroundImage(imageUrl,callback);
	},
	
	/**
	 * Get selected group (all selected objects) on canvas
	 * @note - This feature is turned off by default
	 * @param - options JSON
	 * @return - all selected objects
	 **/
	_getSelectedObjects: function(options){
		return new Array();
	},
	
	/**
	 * Get single selected object on canvas
	 * @param - options JSON
	 * @return - selected object
	 **/
	_getSelectedObject: function(options){
		return Salomay.FabricShapeWrapper.wrap(this.engine.getActiveObject());
	},
	
	/**
	 * Set a object to selected
	 * @param - options JSON
	 **/
	_setSelectedObject: function(options){
		this.engine.setActiveObject(options.shape.getEngineShape());
	},
	
	/**
	 * Cancel selected status for all selected objects
	 * @param - options JSON
	 **/
	_discardAllSelectedObjects: function(options){
		this.engine.discardActiveObject();
	},
	
	/**
	 * Bind canvas to some event and specify the callback
	 * @param - options JSON
	 **/
	_on: function(options) {
		var paramOpt = options;
		var self = this;
		var t = function(options) {
			var shape = Salomay.FabricShapeWrapper.wrap(options.target);
			var event = new Salomay.Event({event:options.e, target: shape});
			if (paramOpt.callback) {
				paramOpt.callback(event);
			}
			
			if (shape) shape.notifyObserver("On_shape_selected", {event: event, canvas: self, shape: shape});
		}
		this.engine.on(paramOpt.eventName, t);
	},
	
	/**
	 * Return the name of engine
	 * @return name of engine (FabricAdapter)
	 **/
	_getEngineType: function(){
		return "FabricAdapter"
	},
	
	/**
	 * Convert engine object to JSON
	 * @return JSON data of object
	 **/
	_toJSON: function(){
		return this.engine.toJSON();
	},
	
	/**
	 * Load data from JSON into engine
	 * @options JSON options
	 **/
	_loadFromJSON: function(options) {
		var JSON = options.json;
		var callback = options.callback ? options.callback : (function(){this.engine.renderAll();}).bind(this);
		this.engine.loadFromJSON(JSON, callback);
	},
	
	/**
	 * Get a list of shapes paint on current engine
	 * @return Salomay shape object list
	 **/
	_getAllObjects: function(){
		var objects = this.engine.getObjects();
		var returnValues = new Array();
		for (var i=0; i < objects.length; i++) {
			returnValues[returnValues.length] = Salomay.FabricShapeWrapper.wrap(objects[i]);
		}
		return returnValues;
	},
	
	/**
	 * Find target from event
	 * @param event DOM Event object
	 * @return Salomay shape object
	 **/
	_findTarget: function(event) {
		var target = this.engine.findTarget(event);
		if (target) {
			return Salomay.FabricShapeWrapper.wrap(target);
		}
		return null;
	},
	
	/**
	 * Set brush options for canvas
	 * @param options brush options
	 **/
	_setBrush: function(options) {
		var type = options.type;
		var width = Number(options.width);
		var color = options.color.toString();
		color = new fabric.Color(color);
		color = "#" + color.toHex();
		if (type == "Pencil") width = width/10;
		if (type == "Spray") {
			/** Original implementation of fabric spray brush **/
			//this.engine.freeDrawingBrush = new fabric['SprayBrush'](this.engine);
			//this.engine.freeDrawingBrush.density = 8;
			//this.engine.freeDrawingBrush.dotWidth = 2;
			//this.engine.freeDrawingBrush.dotWidthVariance = 2;
			
			
			/** Override fabric spray brush with better rendering speed **/
			var points = new Array();
			for (var i = 0; i < 40; i++) { 
				var x = Math.random();
				var y = Math.random();
				var o = Math.random();
				points[points.length] = x + "," + y + "," + o +"|";
			}
			
			this.engine.freeDrawingBrush = new fabric.PatternBrush(this.engine);
			this.engine.freeDrawingBrush.width = parseInt(width * 20, 10) || 10;
			this.engine.freeDrawingBrush.randomPoints = points;
			this.engine.freeDrawingBrush.getPatternSrc = function() {
				var squareWidth = this.width;
				var points = this.randomPoints.split("|");
				var patternCanvas = fabric.document.createElement('canvas');
				patternCanvas.width = patternCanvas.height = squareWidth;
				var ctx = patternCanvas.getContext('2d');	
				ctx.fillStyle = this.color;   
				for (var i = 0; i < 40; i++) {
					var arr = points[i].split(",");
					if (points[i].indexOf(",") == 0) {
						x = Number(arr[1]) * squareWidth;
						y = Number(arr[2]) * squareWidth;
						o = Number(arr[3]);
					} else {
						x = Number(arr[0]) * squareWidth;
						y = Number(arr[1]) * squareWidth;
						o = Number(arr[2]);
					}
					ctx.globalAlpha = o;
					ctx.rect(1+ x - 10 + i, 1 + y - 10 + i, 2, 2);
					ctx.fill();
				}
				return patternCanvas;
			}
		} else {
			this.engine.freeDrawingBrush = new fabric[type + 'Brush'](this.engine);
		}
		this.engine.freeDrawingBrush.color = color;
		this.engine.freeDrawingBrush.width = parseInt(width * 20, 10) || 10;
	}
}, Salomay.EngineInterface.prototype);


/**
 * Salomay shape instance builder, DO NOT new shape class in client code. You must use this object to 
 * create the shape instance
 * @author Cheng Li, Sep 06, 2013
 **/
Salomay.ShapeFactory = {
	// Shape types constants
	SHAPE_RECT : "rectangle",
	SHAPE_POLYGON : "polygon",
	SHAPE_TEXT : "text",
	SHAPE_PENCIL: "pencil",
	SHAPE_LINE: "line",
	SHAPE_CIRCLE : "circle",
	SHAPE_TRIANGLE: "triangle",
	SHAPE_PATH: "path",
	// Static properties
	shapeIndex: 0,
	
	
	/**
	 * All shape instances should have property id to identiy the instance
	 * this method maints a id counter and set id property for salomay object if it doesn't have an id.
	 * @param shape Salomay shape object
	 **/
	buildId: function(shape) {
		if (!shape.getProperty("id")) {
			Salomay.ShapeFactory.shapeIndex++;
			shape.setProperty("id", shape.getShapeType()    + "_" + Salomay.ShapeFactory.shapeIndex);
		}
	},
	
	/**
	 * Factory method, please use it to create any salomay shape object you want
	 * @param options build options, includes shape details and render engine information
	 * @return Salomay shape built by method
	 **/
	Create: function(options) {
		var width = options.width ? options.width : 1;
		var height = options.height ? options.height : 1;
		var left = options.left ? options.left : 50;
		var top = options.top ? options.top : 50;
		//var fill = options.fill ? options.fill : "rgba(255,0,0,0.5)";
		var type = options.type ? options.type : Salomay.ShapeFactory.SHAPE_RECT;
		var borderWidth = options.borderWidth ? Number(options.borderWidth) : 1;
		var borderColor = options.borderColor ? options.borderColor.toString() : '#000000';
		var fillColor = options.fillColor ? options.fillColor.toString() : '#FFFFFF';
		var opacity = options.opacity ? Number(options.opacity) : 1;
		var shape = null;
		var engine = options.engine;
		var text = options.text ? options.text : "";
		var points = options.points ? options.points : new Array();
		var selectable = options.selectable === false ?	 false : true;
		var fontSize = options.fontSize ? options.fontSize : 20;
		var visible = options.visible ? options.visible : true;
		// Build the shape instance for different rendering engine
		if (engine.getEngineType() == "FabricAdapter") {
			switch (options.type){
				case Salomay.ShapeFactory.SHAPE_TRIANGLE:
					shape = new Salomay.Triangle({
					   width: width, height: height, left: left, top: top, angle: 0, strokeWidth : borderWidth, stroke : borderColor,
					   fill: fillColor,
					   opacity: opacity,
					   selectable: selectable
					});
				break;
				case Salomay.ShapeFactory.SHAPE_CIRCLE:
					shape = new Salomay.Circle({
					   radius: height, left: left, top: top, angle: 0, strokeWidth : borderWidth/50, stroke : borderColor,
					   fill: fillColor,
					   opacity: opacity,
					   selectable: selectable
					});
				break;
				case Salomay.ShapeFactory.SHAPE_TEXT:
					if (text && text.length > 0) {
						shape = new Salomay.Text({
						   left: left, top: top, angle: 0, strokeWidth : borderWidth, stroke : borderColor, text: text,
						   fill: fillColor,
						   opacity: opacity,
						   selectable: selectable,
						   fontSize: fontSize
						});
					}
				break;
				case Salomay.ShapeFactory.SHAPE_LINE:
					shape = new Salomay.Line({
						points: points,
						strokeWidth : borderWidth, 
						stroke : borderColor,
						fill: fillColor,
						opacity: opacity,
					    selectable: selectable
					});
				break;
				case Salomay.ShapeFactory.SHAPE_REC:
				break;
				default:
					shape = new Salomay.Rect({
					  width: width, height: height, left: left, top: top, angle: 0, strokeWidth : borderWidth, stroke : borderColor,
					  fill: fillColor,
					  opacity: opacity,
					  selectable: selectable
					});
				break;
			}
		}
		if (shape) {
			//Salomay.ShapeFactory.buildId(shape);
		}
		return shape;
	},

	/**
	 * Reset factory status
	 **/
	reset: function(){
		Salomay.ShapeFactory.shapeIndex = 0;
	}
}


/**
 * The implementation of interface Salomay.Shape and based on open source framework Fabric.
 * this class is an abstract class and supplies generic methods for all child salomay shape classes
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.FabricShapeAdapter = Class.create();
Salomay.FabricShapeAdapter.prototype = Class.extend({
	// Property to mark the object is a fabric shape adapter
	isFabricShapeAdapter: true,
	
	/**
	 * Constructor
	 **/
	initialize: function(options){ 
		 this.observerManager = new ObserverManager();
		 this.options = options;
		 if (Salomay.FabricShapeWrapper.isFabricObject(options)) {
			this.engineShapeObject = options;
		 } else {
			this._initialize(options);
		 }
		 this.shapeType = this.getShapeType();
		 this.bindEvents();
		 Salomay.ShapeFactory.buildId(this);
	},
	
	/**
	 * Return fabric shape object
	 * @return the wrapped fabric shape instance
	 **/
	getEngineShape: function(){
		return this.engineShapeObject;
	},
	
	/**
	 * Set property for shape instance
	 * @param name name of property
	 * @param value value of property
	 **/
	_setProperty: function(name, value){
		this.engineShapeObject.set(name, value);
	},
	
	/**
	 * Get property of shape instance
	 * @param name name of property
	 * @return value of property
	 **/
	_getProperty: function(name){
		return this.engineShapeObject.get(name);
	},
	
	/**
	 * Use this method to lock the location of object so it can not be moved.
	 **/
	_lockMovement: function(){
		this.engineShapeObject.lockMovementX = true;
		this.engineShapeObject.lockMovementY = true;
	},
	
	/**
	 * Use this method to unlock the object so it can be moved
	 **/
	_unlockMovement: function(){
		this.engineShapeObject.lockMovementX = false;
		this.engineShapeObject.lockMovementY = false;
	},
	
	/**
	 * Render the shape (itself) to canvas
	 * @param canvas canvas engine which use to draw the shape
	 **/
	_renderToCanvas: function(canvas){
		canvas.getEngine().add(this.engineShapeObject);
	},
	
	/**
	 * Remove the shape (itself) from canvas
	 * @param canvas canvas engine which use to draw the shape
	 **/
	_removeFromCanvas: function(canvas) {
		canvas.getEngine().remove(this.engineShapeObject);
	},
	
	/**
	 * This method defines how to scale shape during drag and drop mouse on canvas
	 * @param - x offset between start mouse x value and current mouse x value
	 * @param - y offset between start mouse y value and current mouse y value
	 **/
	_scaleWithDrag: function(xOffset, yOffset) {
		this.engineShapeObject.width  = xOffset;
		this.engineShapeObject.height  = yOffset;
		this.engineShapeObject.scale(2);
	},
	
	/**
	 * Call this method to display a shape on canvas
	 **/
	_show: function() {
		this.engineShapeObject.visible = true;
	},
	
	/**
	 * Call this method to hide a shape on canvas
	 **/
	_hide: function() {
		this.engineShapeObject.visible = false;
	},
	
	/**
	 * Bind default events for shape
	 **/
	_bindEvents: function(){
		//this.engineShapeObject.on("object:selected",function(options){alert(options.id)})
	},
	
	/**
	 * Get fill color of shape
	 * @return shape color Hex value
	 **/
	_getFillColor:function() {
		return this.getProperty("fill");
	},
	
	/**
	 * Get border color of shape
	 * @return shape color Hex value
	 **/
	_getBorderColor:function() {
		return this.getProperty("stroke");
	},
	
	/**
	 * Get border width of shape
	 * @return shape width
	 **/
	_getBorderWidth:function() {
		return this.getProperty("strokeWidth");
	},
	
	/**
	 * Get opacity of shape
	 * @return shape opacity
	 **/
	_getOpacity:function() {
		return this.getProperty("opacity");
	},
	
	/**
	 * Set fill color of shape
	 * @param color Hex string
	 **/
	_setFillColor:function(color) {
		this.setProperty("fill", color);
	},
	
	/**
	 * Set border color of shape
	 * @param color Hex string
	 **/
	setBorderColor:function(color) {
		this.setProperty("stroke", color);
	},
	
	/**
	 * Set border width of shape
	 * @param width of shape border
	 **/
	_setBorderWidth:function(width) {
	   this.setProperty("strokeWidth", width);
	},
	
	/**
	 * Set opacity of shape
	 * @param opacity shape opacity
	 **/
	_setOpacity:function(opacity) {
	    this.setProperty("opacity", opacity);
	},
	
	/**
	 * Adjust down Z level
	 */
	_sendBackwards: function(){
		this.engineShapeObject.sendBackwards();
	},
	
	/**
	 * Adjust up Z level
	 */
	_bringForward: function(){
		this.engineShapeObject.bringForward();
	},
	
	/**
	 * Returns visiablity of shape
	 * @return true if visble and false if hidden
	 **/
	_isVisible: function() {
		return this.engineShapeObject.visible;
	},
	
	/**
	 * Bind shape to some event and specify the callback
	 * @param - options JSON
	 **/
	_on: function(options) {
		var paramOpt = options;
		var self = this;
		var t = function(options) {
			var shape = Salomay.FabricShapeWrapper.wrap(options.target);
			var event = new Salomay.Event({event:options.e, target: shape});
			if (paramOpt.callback) {
				paramOpt.callback(event);
			}
		}
		this.engineShapeObject.on(paramOpt.eventName, t);
	},
	
	/**
	 * Get the center point of shape instance
	 * @return center point
	 **/
	_getCenterPoint: function(){
		return this.engineShapeObject.getCenterPoint();
	},
	
	/**
	 * Get wrapped rectagnle
	 **/
	_getBoundingRect: function() {
		return this.engineShapeObject.getBoundingRect();
	},
	
	/** 
	 * Clone current shape with new ID
	 **/
	_clone: function(){
		var newShape = Salomay.FabricShapeWrapper.wrap(this.engineShapeObject.clone());
		newShape.setProperty("id", newShape.getProperty("id") + "_copy");
		return newShape;
	}
}, Salomay.ShapeInterface.prototype);

/**
 * Static wrapper class, use it to wrap original fabric with Salomay shape adapter
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.FabricShapeWrapper = {
	/**
	 * Check if the given object is fabric shape instance
	 * @param object the shape object going to be checked
	 **/
	isFabricObject: function(object) {
		var shapeName = object.toString();
		if (shapeName.indexOf("#<fabric.") != -1) {
			return true;
		}
		return false;
	},
	
	/**
	 * This method receives fabric shape object as parameter and converts it to Salomay shape adapter object
	 * according to shape type name
	 * @param object the shape object going to be converted
	 **/
	wrap: function(shape) {
		var wrappedShape = null;
		if (shape) {
			var shapeName = shape.toString();
			if (Salomay.FabricShapeWrapper.isFabricObject(shape)) {
				var newShapeName = "Salomay." + shapeName.replace("#<fabric.", "").replace(">", "");
				if (newShapeName.indexOf("Path") != -1) newShapeName = "Salomay.Path";
				if (newShapeName.indexOf("Text") != -1) newShapeName = "Salomay.Text";
				if (newShapeName.indexOf("Line") != -1) newShapeName = "Salomay.Line";
				eval ("wrappedShape = new " + newShapeName + "(shape)");
			}	
		}
		return wrappedShape;
	}
}

/**
 * Rectangle adapter implementation
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.Rect = Class.create();
Salomay.Rect.prototype = Class.extend({
	_initialize: function(options){
		this.engineShapeObject = new fabric.Rect(options);
	},
	
	_getShapeType: function(){
		return Salomay.ShapeFactory.SHAPE_RECT;
	}
}, Salomay.FabricShapeAdapter.prototype);

/**
 * Triangle adapter implementation
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.Triangle = Class.create();
Salomay.Triangle.prototype = Class.extend({
	_initialize: function(options){
		this.engineShapeObject = new fabric.Triangle(options);
	},
	
	_getShapeType: function(){
		return Salomay.ShapeFactory.SHAPE_TRIANGLE;
	}
}, Salomay.FabricShapeAdapter.prototype);

/**
 * Circle adapter implementation
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.Circle = Class.create();
Salomay.Circle.prototype = Class.extend({
	_initialize: function(options){
		this.engineShapeObject = new fabric.Circle(options); 
	},
	/** 
	 * Override default implementation of Salomay.FabricShapeAdapter
	 **/
	_scaleWithDrag: function(xOffset, yOffset) {
		this.engineShapeObject.scale((xOffset + yOffset));
	},
	
	_getShapeType: function(){
		return Salomay.ShapeFactory.SHAPE_CIRCLE;
	}
}, Salomay.FabricShapeAdapter.prototype);

/**
 * Path adapter implementation
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.Path = Class.create();
Salomay.Path.prototype = Class.extend({
	_initialize: function(options){
		this.engineShapeObject = new fabric.Path(options); 
	},
	
	_getShapeType: function(){
		return Salomay.ShapeFactory.SHAPE_PATH;
	},
	
	isSpray: function(){
		if (this.engineShapeObject.stroke && this.engineShapeObject.stroke.source) {
			return true;
		}
		return false;
	},
	
	setBorderColor: function (color) {
		if (!this.isSpray()) {
			this.setProperty("stroke", color);
		} else {
			var funStr = String(this.getProperty("stroke").source);
			var currentColor = funStr.substr(funStr.indexOf('"#'),10);
			var c = new fabric.Color(color);
			funStr = funStr.replace(currentColor,'"#' + c.toHex() + '";');
			this.engineShapeObject.stroke = new fabric.Pattern({
			  source: funStr
			});
		}
	},
	
	_getBorderColor: function(){
		if (this.isSpray()) {
			var funStr = String(this.getProperty("stroke").source);
			var currentColor = funStr.substr(funStr.indexOf('"#'),10);
			currentColor = currentColor.replace('"#',"").replace('";',"");
			return currentColor
		} else {
			return this.getProperty("stroke");
		}
	},
	
	_clone: function(){
	   var newShape = null;
	   this.engineShapeObject.clone((function (clone) {
			newShape = Salomay.FabricShapeWrapper.wrap(clone);
	   }).bind(this))
	   newShape.setProperty("id", newShape.getProperty("id") + "_copy");
	   return newShape;
	}
}, Salomay.FabricShapeAdapter.prototype);


/**
 * Text adapter implementation
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.Text = Class.create();
Salomay.Text.prototype = Class.extend({
	_initialize: function(options){
		this.engineShapeObject = new fabric.Text(options.text, options); 
	},
	
	_getShapeType: function(){
		return Salomay.ShapeFactory.SHAPE_TEXT;
	},
	
	getText: function(){
		return this.engineShapeObject.getText();
	}
}, Salomay.FabricShapeAdapter.prototype);


/**
 * Line adapter implementation
 * @author - Cheng Li, Sep 07, 2013
 **/
Salomay.Line = Class.create();
Salomay.Line.prototype = Class.extend({
	_initialize: function(options){
		this.engineShapeObject = new fabric.Line(options.points, options); 
	},
	
	_getShapeType: function(){
		return Salomay.ShapeFactory.SHAPE_LINE;
	},
	
	
	setPoint: function(options) {
		this.engineShapeObject.set(options);
	}
}, Salomay.FabricShapeAdapter.prototype);